﻿using System;
using System.Diagnostics;
using System.Collections.Generic;

namespace Heaven.Diagnostics
{
    /// <summary>
    /// Контейнер для результата 
    /// отдельного контрольного теста
    /// </summary>
    public struct SubtestResult
    {
        #region Атрибуты

        /// <summary>
        /// Результат
        /// </summary>
        private TestState result;
        /// <summary>
        /// Время прохождения теста (мс)
        /// </summary>
        private double elapsedtime;
        /// <summary>
        /// Название субтеста
        /// </summary>
        private string name;
        /// <summary>
        /// Ожидаемое значение
        /// </summary>
        private string expected;
        /// <summary>
        /// Полученое значение
        /// </summary>
        private string actual;


        #endregion

        #region Свойства

        /// <summary>
        /// Результат субтеста
        /// </summary>
        public TestState Result
        {
            get { return result; }
        }
        /// <summary>
        /// Время от запуска теста (мс)
        /// </summary>
        public double ElapsedTime
        {
            get { return elapsedtime; }
        }
        /// <summary>
        /// Название субтеста
        /// </summary>
        public string Name
        {
            get { return name;  }
        }
        /// <summary>
        /// Ожидаемое значение
        /// </summary>
        public string Expected
        {
            get { return expected; }
        }
        /// <summary>
        /// Полученое значение
        /// </summary>
        public string Actual
        {
            get { return actual; }
        }

        #endregion

        #region Инициализация
        
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="result">Результат теста</param>
        /// <param name="elapsed">Время от начала теста (в мс)</param>
        /// <param name="name">Название субтеста</param>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Полученное значение</param>
        public SubtestResult(TestState result, double elapsed, string name, string expected, string actual)
        {
            this.result = result;
            this.elapsedtime = elapsed;
            this.name = name;
            this.actual = actual;
            this.expected = expected;
        }

        #endregion
    }

    /// <summary>
    /// Аккумулятор тестов. Накапливает 
    /// информацию об отдельном тесте
    /// </summary>
    public class Collector
    {
        #region Атрибуты

        // Список сообщений
        List<SubtestResult> subtests = new List<SubtestResult>();
        // Результат
        TestState result = TestState.Untested;
        // Исключение (если  произошло)
        Exception exception = null;

        // Таймер для замера производительности
        Stopwatch stopwatch = new Stopwatch();

        #endregion

        #region Свойства

        /// <summary>
        /// Произошедшее исключение во время теста
        /// </summary>
        public Exception Exception
        {
            get { return exception; }
            set 
            {
                result = TestState.Failed;
                exception = value; 
            }
        }

        /// <summary>
        /// Результат тестироания
        /// </summary>
        public TestState Result
        {
            get { return result; }
        }

        /// <summary>
        /// Результаты набора тестов
        /// </summary>
        public List<SubtestResult> Subtests
        {
            get { return subtests; }
        }

        #endregion

        #region Методы

        /// <summary>
        /// Обозначение начала тестов. Метод может 
        /// быть использован для явного указания начала 
        /// блока тестов в случае, например, 
        /// подготовки данных для тестов
        /// </summary>
        public void Start()
        {
            // Стартуем отсчет
            stopwatch.Start();
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(int expected, int actual, string message)
        {
            Inspect(expected == actual, expected.ToString(), actual.ToString(), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(bool expected, bool actual, string message)
        {
            Inspect(expected == actual, expected.ToString(), actual.ToString(), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(string expected, string actual, string message)
        {
            Inspect(expected == actual, expected.ToString(), actual.ToString(), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(long expected, long actual, string message)
        {
            Inspect(expected == actual, expected.ToString(), actual.ToString(), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="tolerance">Допускаемое отклонение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(float expected, float actual, float tolerance, string message)
        {
            bool compare = Math.Abs(expected - actual) <= tolerance;
            Inspect(compare, expected.ToString("0.#####") + " ~" + tolerance.ToString("0.#####"), actual.ToString("0.#####"), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="tolerance">Допускаемое отклонение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(decimal expected, decimal actual, decimal tolerance, string message)
        {
            bool compare = Math.Abs(expected - actual) <= tolerance;
            Inspect(compare, expected.ToString("0.#####") + " ~" + tolerance.ToString("0.#####"), actual.ToString("0.#####"), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="expected">Ожидаемое значение</param>
        /// <param name="actual">Актуальное/полученное значение</param>
        /// <param name="tolerance">Допускаемое отклонение</param>
        /// <param name="message">Название теста</param>
        public void Inspect(double expected, double actual, double tolerance, string message)
        {
            bool compare = Math.Abs(expected - actual) <= tolerance;
            Inspect(compare, expected.ToString("0.#####") + " ~" + tolerance.ToString("0.#####"), actual.ToString("0.#####"), message);
        }

        /// <summary>
        /// Контроль результатов теста
        /// </summary>
        /// <param name="condition">Условие прохождения теста</param>
        /// <param name="message">Название теста</param>
        public void Inspect(bool condition, string message)
        {
            subtests.Add(new SubtestResult(
                condition ? TestState.Successful : TestState.Failed, 
                GetElapsedTime(), message, "True", condition ? "True" : "False"));
            if (!condition) result = TestState.Failed;
            else if (result != TestState.Failed) result = TestState.Successful;
        }

        #endregion

        #region Инициализация

        /// <summary>
        /// Конструктор по умолчанию
        /// </summary>
        public Collector()
        {

        }

        #endregion

        #region Вспомогательные

        /// <summary>
        /// Получить пройденое время с момента 
        /// последнего вызова этой функции
        /// </summary>
        /// <returns>Время в мс</returns>
        private double GetElapsedTime()
        {
            // Получаем из тиков секунды и переводим их в мс
            double elapsed = Convert.ToDouble(
                (double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency) * 1000.0;
            // Начинаем отсчет времени 
            // до следующего теста
            stopwatch.Reset();
            stopwatch.Start();

            return elapsed;
        }

        private void Inspect(bool ok, string expected, string actual, string message)
        {
            subtests.Add(new SubtestResult(
                ok ? TestState.Successful : TestState.Failed,
                GetElapsedTime(), message, expected, actual));
            if (!ok) result = TestState.Failed;
            else if (result != TestState.Failed) result = TestState.Successful;
        }

        #endregion
    }
}
